package day1219_test;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

public class Server2 {

    //一.main方法调用构造方法创建服务端(开放某端口)， 然后调用start方法等待客户端连接
    // (如果客户端连接了，就启动一个线程处理此客户端的请求)
    public static void main(String[] args) {
        Server2 ser=new Server2();//1.创建服务器端对象
        ser.start();//2.用服务器端对象调用start()
    }


    //二.服务器端主要通过ServerSocket   ss启动(并用ServerSocket   ss的accept方法接收客户端的连接)
    ServerSocket   ss=null;
    Server2(){
        System.out.println("正在启动服务器...");
        try {//2.1.用ServerSocket对象开启8088端口(客户端只能通过8088端口和此服务器连接)
            ss=new ServerSocket(8088);//电脑会使用0--65535之间的端口，一个程序中的多个进程会采用其中的几个端口号进行通信
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("启动服务器成功...");
    }


    //三.start方法用于等待客户端的连接并处理连接客户端的请求
    //(如果客户端连接了，就启动一个线程处理此客户端的请求)
    public void start(){
        try {
            while(true){
                System.out.println("服务器等待客户端连接...");
                //3.1.等待并接收客户端的连接(客户端连接上服务端accept方法就会接收到一个Socket对象<此对象
                // 中包含输入流用于读取客户端的数据和输出流用于给客户端返回数据>)
                Socket s=ss.accept();
                System.out.println(s.getInetAddress()+"连接成功了");

                //3.2.客户端连接上服务端，就启动一个ClientService线程为此客户端服务(和此客户端通信)
                //ClientService线程负责接收连接的客户端的数据并将数据转发给所有客户端
                ClientService  cs=new ClientService(s);
                cs.start();//用start方法启动线程
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }




    //四.保存每个客户端对应的输出流(类似于老师的通讯录<保存了每个学生的手机号>)
    ArrayList<PrintWriter>   allOut=new ArrayList();

    //五.线程类负责用和某客户端对应socket中获取输出流存入到ArrayList allOut集合，
    //然后从和某客户端对应socket中获取输入流读取此客户端发送的数据并用集合中的所有输出流将读取的数据发送给每个客户端
    class  ClientService  extends  Thread{
        Socket  socket;//1.需要给线程里边传入一个Socket(用于获取输入流和输出流<用输入流读取客户端发送的数据, 输出流存入到集合用于群发>)
        ClientService(Socket  s){
            socket=s;
        }
        public void run() {
            //2.用socket获取输入流(读取客户端的数据)和输出流(放入集合, 然后可以从集合中获取每个客户端对应的输出流，用输出流给每个客户端转发数据)
            PrintWriter  pw=null;
            try {
                //2.1.用Socket对象获取输出流(后边会将此输出流放入ArrayList集合)
                //(先从socket中获取字节输出流，再将字节输出流封装为字符输出流<用于更加方便的发送字符汉字等>，
                // 再将字符输出流封装为PrintWriter<用于使用println方法按行输出字符数据>)
                OutputStream  out=socket.getOutputStream();//从socket中获取输出流
                OutputStreamWriter osw=new OutputStreamWriter(out,"UTF-8");
                pw=new PrintWriter(osw);
                //2.2.将输出流放入ArrayList allOut集合(用于后边从集合中获取每个输出流给对应的每个客户端转发数据<群发数据>)
                synchronized (allOut){//因为会有多个ClientService线程都操作ArrayList allOut集合，
                    allOut.add(pw); //所以给集合添加一个线程锁，让此集合操作是线程安全的(不能让多个线程都操作此集合，类似于集合中存储了商品进行秒杀原理一样)
                    System.out.println("在线的人数为：" + allOut.size());
                }

                //2.3.用Socket对象获取输入流(用ClientService线程通过此输入流不断读取此线程对应的客户端发送的数据)
                //                       (类似于老师通过电话不倾听听领导发送的公告)
                InputStream in=socket.getInputStream();
                InputStreamReader isr=new InputStreamReader(in,"utf-8");
                BufferedReader  br=new BufferedReader(isr);
                //2.4.用ClientService线程通过此输入流不断读取此线程对应的客户端发送的数据
                //(如果读取到了就打印，并返回到所有客户端)(如果没有读取到数据循环就会阻塞，等待客户端发送数据<直到客户端发送数据过来再循环>)
                //(如果windows客户端断开，则br.readLine()会抛出异常，就继续执行finally代码<从集合中删除此客户端对应的输出流，表示此客户端下线>)
                //(如果Linux客户端断开，则br.readLine()会读取到null值，while循环就会终止，同样继续执行finally代码<从集合中删除此客户端对应的输出流，表示此客户端下线>)
                String msg=null;
                while(  (msg=br.readLine()) !=null   ){//while条件里边，用输入流br的readLine()方法读取数据并存入到Sting msg变量中。如果读取的数据不为null就处理
                    System.out.println(msg);//2.4.1.如果读取的数据不为null就打印此线程对应的客户端发送的数据
                    //2.4.2.然后从ArrayList allOut集合中获取每个客户端对应的输出流PrintWriter  pwFromAll，用输出流pwFromAll将接收的数据msg输出给每个客户端
                    synchronized (allOut){
                        for(int i=0;i<allOut.size();i++){
                            PrintWriter  pwFromAll=allOut.get(i);
                            pwFromAll.println(msg);
                            pwFromAll.flush();//flush()方法的作用是：如果线程只是将要输出的数据暂存到电脑内存中等待输出，则强制用flush方法将数据输出到客户端
                        }
                    }
                }

            } catch (IOException e) {
                System.out.println("有一个客户端下线了");
            }finally {
                //2.5.如果某个客户端关闭了(while循环的br.readLine()就会抛出异常或者读取到null，则就会执行finally代码),
                // 则再finally中把它对应的输出流从集合中删除(表示客户端下线了)(并且把socket对象从内存中删除，由socket.close方法 完成)
                //调用socket.close()是为了把socket对象从内存中删除(就是服务端不要在和这个已经关闭的客户端进行数据交互了<否则就会出异常>)
                synchronized (allOut){
                    allOut.remove(pw);
                    System.out.println("在线的人数为：" + allOut.size());
                }
                try {
                    //调用socket.close()是为了把socket对象从内存中删除(就是服务端不要在和这个已经关闭的客户端进行数据交互了<否则就会出异常>)
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }



}


